package service.impl;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import entity.data.DocFile;
import entity.inte.ResponseData;
import entity.inte.mycloud.block.BatchUpdateBlocksRequest;
import entity.inte.mycloud.block.UpdateTextElement;
import entity.inte.mycloud.doc.block.Element;
import entity.inte.mycloud.doc.block.Link;
import entity.inte.mycloud.doc.block.TextElementStyle;
import entity.inte.mycloud.doc.block.TextRun;
import entity.inte.mycloud.file.DeleteFileResponse;
import entity.inte.mycloud.imports.CreateImportTasksRequest;
import entity.inte.mycloud.imports.CreateImportTasksResponse;
import entity.inte.mycloud.imports.QueryImportTasksResponse;
import entity.inte.mycloud.upload.UploadFileRequest;
import entity.inte.mycloud.upload.UploadFileResponse;
import org.apache.commons.codec.CharEncoding;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import service.FileService;
import session.SessionData;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class FileServiceImpl implements FileService {
	private static final Logger LOGGER = Logger.getLogger(FileService.class.getName());
	private static FileService INSTANCE;
	public static final String UPLOAD_FILE = SessionData.BASE_URL + "/drive/v1/files/upload_all";
	public static final String CREATE_IMPORT_TASKS = "https://open.feishu.cn/open-apis/drive/v1/import_tasks";
	public static final String QUERY_IMPORT_TASKS = "https://open.feishu.cn/open-apis/drive/v1/import_tasks/";
	public static final String GET_ALL_BLOCKS = "https://open.feishu.cn/open-apis/docx/v1/documents/:document_id/blocks";
	public static final String BATCH_BLOCKS = "https://open.feishu.cn/open-apis/docx/v1/documents/:document_id/blocks/batch_update";
	public static final String DELETE_FILE = "https://open.feishu.cn/open-apis/drive/v1/files/:file_token";
	/** 批量更新的最大长度 */
	public static final int BATCH_UPDATE_LENGTH = 200;
	private static final HttpClient httpClient = HttpClientBuilder.create().build();
	/** 需要进行重试的错误码及描述映射 */
	public static final HashMap<Integer, String> ERR_CODE = new HashMap<>();

	// 本来是打算直接正则匹配JSON数据的，后来写了对应的实体类，也就不使用正则了
	// public static final Pattern URL_PATTERN = Pattern.compile("\"link\"[\\S\\s]+?\"url\".+?\"(.+?)\"");

	private FileServiceImpl() {
		// 云空间文件夹中不支持并发调用新建在线文档、复制文件、移动文件、新建文件夹、删除文件以及上传文件等接口
		ERR_CODE.put(99991400, "触发接口频控");
		ERR_CODE.put(1061045, "飞书内部可重试错误");
		ERR_CODE.put(1061007, "请确认对应节点未被删除");
		ERR_CODE.put(233523001, "飞书未知错误");
	}

	public static FileService getInstance() {
		if (INSTANCE == null)
			INSTANCE = new FileServiceImpl();
		return INSTANCE;
	}

	// 上传文件
	@Override
	public UploadFileResponse uploadFile(UploadFileRequest uploadFileRequest, int count) {
		LOGGER.info("上传文件请求体\n" + JSONObject.toJSONString(uploadFileRequest));
		String authorization = SessionData.getAppAccessToken().getAuthorization();

		try (FileInputStream inputStream = new FileInputStream(uploadFileRequest.getFile())) {
			String jsonData = Jsoup.connect(UPLOAD_FILE)
					.header("Authorization", authorization) //cookie
					.header("Content-Type", "multipart/form-data;")
					.data("file_name", uploadFileRequest.getFile_name()) //文件名
					.data("parent_type", uploadFileRequest.getParent_type()) //上传点类型=云空间
					.data("parent_node", uploadFileRequest.getParent_node()) //文件夹token
					.data("size", uploadFileRequest.getSize() + "") //文件大小
					.data("file", uploadFileRequest.getFile().getName(), inputStream) //二进制文件数据
					.timeout(60000)
					.ignoreContentType(true)
					.ignoreHttpErrors(true)
					.post()
					.text();

			LOGGER.info("上传文件响应体\n" + jsonData);
			ResponseData<UploadFileResponse> responseData = ResponseData.parseObject(jsonData, UploadFileResponse.class);
			if (responseData.getCode() != 0) {
				String msg = ERR_CODE.get(responseData.getCode());
				if (msg != null) {
					LOGGER.warn(uploadFileRequest.getFile_name() + " 文件上传失败，" + msg + "，重新进行请求");
					return uploadFile(uploadFileRequest, count + 1);
				}
			}

			return responseData.getData();
		} catch (SocketTimeoutException e) {
			if (count > SessionData.MAX_RETRY_COUNT)
				throw new RuntimeException("多次上传文件失败\n" + e);
			return uploadFile(uploadFileRequest, count + 1);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public CreateImportTasksResponse createImportTasks(CreateImportTasksRequest createImportTasksRequest, int count) {
		LOGGER.info("创建导入任务请求体\n" + JSONObject.toJSONString(createImportTasksRequest));
		String authorization = SessionData.getAppAccessToken().getAuthorization();
		try {
			String jsonData = Jsoup.connect(CREATE_IMPORT_TASKS)
					.header("Authorization", authorization) //cookie
					.header("Content-Type", "application/json; charset=utf-8")
					.requestBody(JSONObject.toJSONString(createImportTasksRequest))
					.timeout(60000)
					.ignoreContentType(true)
					.ignoreHttpErrors(true)
					.post()
					.text();

			LOGGER.info("创建导入任务响应体\n" + jsonData);
			return ResponseData.parseObject(jsonData, CreateImportTasksResponse.class).getData();
		} catch (SocketTimeoutException e) {
			if (count > SessionData.MAX_RETRY_COUNT)
				throw new RuntimeException("多次创建导入任务失败\n" + e);
			return createImportTasks(createImportTasksRequest, count + 1);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public QueryImportTasksResponse queryImportTasks(CreateImportTasksResponse createImportTasksResponse, int count) {
		LOGGER.info("查询导入任务请求体\n" + JSONObject.toJSONString(createImportTasksResponse));
		String authorization = SessionData.getAppAccessToken().getAuthorization();
		try {
			String jsonData = Jsoup.connect(QUERY_IMPORT_TASKS + createImportTasksResponse.getTicket())
					.header("Authorization", authorization) //cookie
					.timeout(60000)
					.ignoreContentType(true)
					.ignoreHttpErrors(true)
					.get()
					.text();

			LOGGER.info("查询导入任务响应体\n" + jsonData);
			return ResponseData.parseObject(jsonData, QueryImportTasksResponse.class).getData();
		} catch (SocketTimeoutException e) {
			if (count > SessionData.MAX_RETRY_COUNT)
				throw new RuntimeException("多次查询导入任务失败\n" + e);
			return queryImportTasks(createImportTasksResponse, count + 1);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public DeleteFileResponse deleteDoc(UploadFileResponse uploadFileResponse, int count) {
		LOGGER.info("删除上传文件请求数据\n" + uploadFileResponse.getFile_token());
		String authorization = SessionData.getAppAccessToken().getAuthorization();
		try {
			String jsonData = Jsoup.connect(DELETE_FILE.replace(":file_token", uploadFileResponse.getFile_token()))
					.header("Authorization", authorization)
					.data("type", "file")
					.timeout(20000)
					.ignoreContentType(true)
					.ignoreHttpErrors(true)
					.method(Connection.Method.DELETE)
					.execute()
					.body();

			LOGGER.info("删除上传文件" + uploadFileResponse.getFile_token() + "响应体\n" + jsonData);
			ResponseData<DeleteFileResponse> deleteFileResponseResponseData = ResponseData.parseObject(jsonData, DeleteFileResponse.class);
			if (deleteFileResponseResponseData.getCode() != 0) {
				String msg = ERR_CODE.get(deleteFileResponseResponseData.getCode());
				if (msg != null) {
					LOGGER.warn("删除文件异常，" + msg + "，重新尝试\n");
					deleteDoc(uploadFileResponse, count + 1);
				}
			}
			return deleteFileResponseResponseData.getData();
		} catch (SocketTimeoutException e) {
			if (count > SessionData.MAX_RETRY_COUNT)
				throw new RuntimeException("多次删除 " + uploadFileResponse.getFile_token() + " 上传文件失败\n" + e);
			return deleteDoc(uploadFileResponse, count + 1);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public ResponseData<String> batchUpdateProcess(DocFile docFile, int count) {
		LOGGER.info("获取所有块请求体\n" + JSONObject.toJSONString(docFile.getQueryImportTasksResponse()));
		String authorization = SessionData.getAppAccessToken().getAuthorization();
		QueryImportTasksResponse queryImportTasksResponse = docFile.getQueryImportTasksResponse();
		try {
			String jsonData = Jsoup.connect(GET_ALL_BLOCKS.replace(":document_id", queryImportTasksResponse.getResult().getToken()))
					.header("Authorization", authorization) //cookie
					.data("user_id_type", "user_id")
					.timeout(60000)
					.ignoreContentType(true)
					.ignoreHttpErrors(true)
					.get()
					.text();

			ResponseData<JSONObject> responseData = JSONObject.parseObject(jsonData, ResponseData.class);
			LOGGER.info("获取所有块响应体\n" + new ResponseData<String>(responseData.getCode(), responseData.getMsg(), null));
			// 生成更新块数据
			BatchUpdateBlocksRequest batchUpdateBlocksRequest = processBlocks(docFile, responseData);

			// 不用更新
			if (batchUpdateBlocksRequest.getRequests().length == 0) return new ResponseData<>(0, "success", null);

			// 发送请求
			// 批量更新不能超过最大长度
			if (batchUpdateBlocksRequest.getRequests().length <= BATCH_UPDATE_LENGTH) {
				return batchUpdate(batchUpdateBlocksRequest, queryImportTasksResponse, authorization);
			} else {
				// 分段更新
				Object[] requests = batchUpdateBlocksRequest.getRequests();
				ResponseData<String> response = new ResponseData<>();
				JSONArray jsonArray = new JSONArray();
				for (int i = 0, len = requests.length / BATCH_UPDATE_LENGTH + ((requests.length % BATCH_UPDATE_LENGTH == 0) ? 0 : 1); i < len; i++) {
					BatchUpdateBlocksRequest bubr = new BatchUpdateBlocksRequest(Arrays.copyOfRange(requests, i * BATCH_UPDATE_LENGTH, Math.min((i + 1) * BATCH_UPDATE_LENGTH, requests.length)));
					ResponseData<String> res = batchUpdate(bubr, queryImportTasksResponse, authorization);
					if (res.getCode() != 0) response.setCode(res.getCode());
					if (res.getMsg().equals("success")) response.setMsg(res.getMsg());
					jsonArray.add(res.getData());
				}
				response.setData(jsonArray.toJSONString());
				return response;
			}

		} catch (SocketTimeoutException e) {
			if (count > SessionData.MAX_RETRY_COUNT)
				throw new RuntimeException("多次更新块失败\n" + e);
			return batchUpdateProcess(docFile, count + 1);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	private ResponseData<String> batchUpdate(BatchUpdateBlocksRequest batchUpdateBlocksRequest, QueryImportTasksResponse queryImportTasksResponse, String authorization) throws IOException {
		LOGGER.info("批量更新请求体个数\n" + JSONObject.toJSONString(batchUpdateBlocksRequest.getRequests().length));
		String url = BATCH_BLOCKS.replace(":document_id", queryImportTasksResponse.getResult().getToken());
		url += "?user_id_type=user_id";
		HttpPatch httpPatch = new HttpPatch(url);
		httpPatch.setHeader("Authorization", authorization);
		httpPatch.setHeader("Content-type", "application/json; charset=utf-8");
		httpPatch.setEntity(new StringEntity(JSONObject.toJSONString(batchUpdateBlocksRequest), StandardCharsets.UTF_8));
		HttpResponse response = httpClient.execute(httpPatch);

		HttpEntity entity = response.getEntity();
		String batchUpdateBlocksResponse = EntityUtils.toString(entity, CharEncoding.UTF_8);

		ResponseData<String> responseData = JSONObject.parseObject(batchUpdateBlocksResponse, ResponseData.class);
		// 数据量太大，这里就不打印data了
		LOGGER.info("批量更新响应体\n" + JSONObject.toJSONString(new ResponseData<String>(responseData.getCode(), responseData.getMsg(), null)));
		if (responseData.getCode() != 0) {
			String msg = ERR_CODE.get(responseData.getCode());
			if (msg != null) {
				LOGGER.warn("批量更新响应体失败，" + msg + ", 进行重试");
				return batchUpdate(batchUpdateBlocksRequest, queryImportTasksResponse, authorization);
			}
		}
		responseData.setData(JSONObject.parseObject(batchUpdateBlocksResponse).getString("data"));

		return responseData;
	}

	@Override
	public UploadFileResponse uploadImageMedia(DocFile docFile, int count) {
		return null;
	}

	/**
	 * 处理文本链接和图片的块
	 */
	private BatchUpdateBlocksRequest processBlocks(DocFile docFile, ResponseData<JSONObject> jsonData) {
		Map<String, String> map = docFile.getResourceMap();

		// 没有map就说明没有本地链接或者创建伪造文件时已有重名文件
		if (map == null) return new BatchUpdateBlocksRequest(new Object[0]);
		JSONObject data = jsonData.getData();
		JSONArray items = data.getJSONArray("items");
		Object[] requests = items.stream()
				.map(JSONObject.class::cast)
				.map(jsonObject -> {
					String blockId = jsonObject.getString("block_id");
					// 处理文本
					for (String key : jsonObject.keySet()) { //遍历所有属性
						Object val = jsonObject.get(key);
						if (!(val instanceof JSONObject v)) continue;

						return Optional.ofNullable(v.getJSONArray("elements"))
								.map(Collection::stream)
								.map(elementStream -> {
									Element[] es = elementStream
											.map(JSONObject.class::cast)
											.map(o -> {
												// 不需要更新的数据设置为null
												// 先用伪造的url获取到文件的绝对路径，再通过绝对路径拿到对应的docFile对象，然后获取到其访问的url
												// 不要没有link的数据块
												return Optional.of(TextRun.parseObject(o.getString("text_run")))
														.flatMap(textRun ->
																Optional.ofNullable(textRun.getText_element_style())
																		.flatMap(textElementStyle -> Optional.ofNullable(textElementStyle.getLink())
																				.map(link -> {
																					String url = link.getUrl();
																					if (url == null) return null;
																					String resourceUrl = map.get(url);
																					if (resourceUrl == null)
																						return null;
																					return Optional.ofNullable(SessionData.DOC_FILE_MAP.get(resourceUrl))
																							.map(file -> {
																								String newUrl = file.getQueryImportTasksResponse().getResult().getUrl();

																								if (textRun.getContent() != null && textRun.getContent().contains("汉化说明")) {
																									System.out.println("textRun = " + textRun.getContent());
																									System.out.println("url = " + url);
																									System.out.println("map = " + map);
																									System.out.println("SessionData.DOC_FILE_MAP = " + SessionData.DOC_FILE_MAP);
																									System.out.println("newUrl = " + newUrl);
																								}

																								TextRun tr = new TextRun(textRun.getContent(),
																										new TextElementStyle(null, null, null, null,
																												null, null, null, new Link(newUrl), null));
																								return new Element(tr, null, null, null, null, null, null);
																							}).orElseGet(() -> {
																								// 导入失败的文件就把文档中的伪造链接删掉
																								String newUrl = "";
																								TextRun tr = new TextRun(textRun.getContent(),
																										new TextElementStyle(null, null, null, null,
																												null, null, null, new Link(newUrl), null));
																								return new Element(tr, null, null, null, null, null, null);
																							});
																				}))).orElse(null);
											})
											.filter(Objects::nonNull)
											.toList()
											.toArray(new Element[0]);
									if (es.length == 0) return null;
									return new UpdateTextElement(blockId, new UpdateTextElement.TextElement(es));
								}).orElse(null);
					}

					return null;
				})
				.filter(Objects::nonNull) //过滤到没有url并且不是图片的数据
				.toArray();

		LOGGER.info("块处理后数据个数 \n" + JSONObject.toJSONString(requests.length));
		return new BatchUpdateBlocksRequest(requests);
	}


}
